﻿using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Input;

namespace WinHooksNET
{
    public class KeyboardHook
    {
        #region Members
        public event EventHandler<Events.KeyboardHookEventArgs> OnKeyboardAction;

        private List<Events.KeyboardHookEventArgs> _eventsHistory = new List<Events.KeyboardHookEventArgs>();
        private List<Key> _clickedKeys = new List<Key>();

        private Helpers.PInvokeCode.HookProc _keyboardHookProcHandle;
        private IntPtr _keyboardHookHandle;

        private bool _handleKeyboardKeyPress = true;
        private bool _handleKeyboardKeyRelease = true;

        private Enums.FilterMode _filterMode = Enums.FilterMode.BooleanFilters;
        
        private int _historyLimit = 100;

        private Func<bool> _customFilterFunc;

        /// <summary>
        /// Determine whether the library is to intercept key press
        /// </summary>
        public bool HandleKeyboardKeyPress
        {
            get { return _handleKeyboardKeyPress; }
            set { _handleKeyboardKeyPress = value; }
        }

        /// <summary>
        /// Determine whether the library is to intercept key release
        /// </summary>
        public bool HandleKeyboardKeyRelease
        {
            get { return _handleKeyboardKeyRelease; }
            set { _handleKeyboardKeyRelease = value; }
        }

        /// <summary>
        /// Determine Filter Mode. BooleanFilter for bool variables or CustomFunction for CustomFilterFunc var.
        /// </summary>
        public Enums.FilterMode FilterMode
        {
            get { return _filterMode; }
            set { _filterMode = value; }
        }

        /// <summary>
        /// Custom function used by filter. FilterMode must be set to a CustomFunction
        /// </summary>
        public Func<bool> CustomFilterFunc
        {
            get { return _customFilterFunc; }
            set { _customFilterFunc = value; }
        }

        /// <summary>
        /// List of called events. Limit of items can be set in HistoryLimit var
        /// </summary>
        public List<Events.KeyboardHookEventArgs> EventsHistory
        {
            get { return _eventsHistory; }
            set { _eventsHistory = value; }
        }

        /// <summary>
        /// Limit of items in called event history. If 0, history can be empty
        /// </summary>
        public int HistoryLimit
        {
            get { return _historyLimit; }
            set
            {
                _historyLimit = value;
                if (EventsHistory.Count >= HistoryLimit)
                {
                    EventsHistory.RemoveRange(HistoryLimit, EventsHistory.Count - HistoryLimit);
                }
            }
        }
        #endregion

        public KeyboardHook()
        {            
            _keyboardHookProcHandle = new Helpers.PInvokeCode.HookProc(KeyboardHookProc);

            //Register mouse hook
            _keyboardHookHandle = Helpers.PInvokeCode.SetWindowsHookEx(Helpers.PInvokeCode.HookType.WH_KEYBOARD_LL, 
                _keyboardHookProcHandle, Helpers.Tools.GetCurrentModuleHandle(), 0);

            //If returned handle is smaller than 0, throw exception
            if ((int)_keyboardHookHandle == 0)
                throw new Exceptions.CreateHookException("Returned keyboard hook handle is null");

            _customFilterFunc = new Func<bool>(() => { return true; });
        }

        #region Functions
        private IntPtr KeyboardHookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            //If code is smaller than 0, we have a problem
            if (code >= 0)
            {
                //Get virtual key code from lParam and convert it to System.Windows.Input Key
                int vKey = Marshal.ReadInt32(lParam);
                Key key = KeyInterop.KeyFromVirtualKey(vKey);

                //Init key state
                Enums.KeyState keyState = Enums.KeyState.Unknown;

                //If wParam is 256, key is pressed, if 257, key is released
                if ((int)wParam == 256)
                {
                    keyState = Enums.KeyState.Pressed;
                    _clickedKeys.Add(key);
                }
                else if((int)wParam == 257)
                {
                    keyState = Enums.KeyState.Released;
                    _clickedKeys.RemoveAll(p => p == key);
                }

                //Get capslock and shifts states, and determine whether the key is uppercase or not
                bool capslockState = Convert.ToBoolean(Helpers.PInvokeCode.GetKeyState((int)0x14));
                bool lShiftState = Convert.ToBoolean(Helpers.PInvokeCode.GetKeyState((int)0xA0) >> 1);
                bool rShiftState = Convert.ToBoolean(Helpers.PInvokeCode.GetKeyState((int)0xA1) >> 1);
                if (rShiftState || lShiftState)
                {
                    capslockState = !capslockState;
                }

                if (IsActionAllowed(keyState))
                {
                    Events.KeyboardHookEventArgs eventArgs = new Events.KeyboardHookEventArgs()
                    {
                        Key = key,
                        State = keyState,
                        IsUppercase = capslockState,
                        Time = DateTime.Now.Ticks
                    };

                    AddEventToHistory(eventArgs);

                    //Call OnKeyBoardAction event
                    OnKeyboardAction(this, eventArgs);
                }
            }

            return Helpers.PInvokeCode.CallNextHookEx(_keyboardHookHandle, code, wParam, lParam);
        }

        private bool IsActionAllowed(Enums.KeyState action)
        {
            if (_filterMode == Enums.FilterMode.BooleanFilters)
            {
                if (((action == Enums.KeyState.Pressed) && HandleKeyboardKeyPress) ||
                    ((action == Enums.KeyState.Released) && HandleKeyboardKeyRelease))
                {
                    return true;
                }
            }
            else if (_filterMode == Enums.FilterMode.CustomFunction)
            {
                bool result = _customFilterFunc();
                return result;
            }

            return false;
        }

        private void AddEventToHistory(Events.KeyboardHookEventArgs _eventArgs)
        {
            if (HistoryLimit != 0)
            {
                if (EventsHistory.Count >= HistoryLimit)
                {
                    if (EventsHistory.Count > 0)
                    {
                        EventsHistory.RemoveAt(EventsHistory.Count - 1);
                    }
                }
                EventsHistory.Insert(0, _eventArgs);
            }
        }

        public Enums.KeyState GetKeyState(Key key)
        {
            if (_clickedKeys.Exists(p => p == key))
            {
                return Enums.KeyState.Pressed;
            }

            return Enums.KeyState.Released;
        }
        #endregion

        /// <summary>
        /// Dispose class and unhook keyboard hook
        /// </summary>
        public void Dispose()
        {
            //Unhook keyboard hook
            Helpers.PInvokeCode.UnhookWindowsHookEx(_keyboardHookHandle);
        }
    }
}
